iT邦幫忙

2024 iThome 鐵人賽

DAY 22
0
JavaScript

從PM到前端開發:我的React作品集之旅系列 第 22

Day 22: 自製 Swiper UI 組件,打造靈活的專案展示頁面

  • 分享至 

  • xImage
  •  

在前一篇文章中,我們優化了 Service Section,並展示了如何使用 CSS Grid Layout 來構建響應式卡片佈局。今天,我們將進一步進階,自己實作一個類似 Swiper.js 的滑動元件,並根據需求自定義屬性。這個元件將允許我們靈活地顯示項目、設定自動播放、無限循環等效果,並且不依賴於第三方庫。

Swiper 元件屬性表

在這篇文章中,我們實作了類似於 Swiper.js 的滑動元件,以下是 Swiper 元件的屬性表,這些屬性能夠靈活地控制每個滑動項目的行為和外觀。

屬性 說明 類型 預設值
items 要顯示的資料陣列,每個項目都包含 idtitledescriptionimage 等屬性 array []
loop 控制是否無限循環滑動 boolean false
autoplay 控制是否自動播放滑動 boolean false
delay 自動播放的延遲時間(以毫秒為單位) number 4000
direction 滑動的方向,可選 horizontalvertical string horizontal
onSlideChange 當滑動發生時觸發的回調函數,回傳當前滑動項的索引 function null

實際演練

Step 1: 初始化 React 元件

我們將首先定義一個 Swiper 元件,並將滑動項目資料作為 props 傳入。這個元件將會使用 CSS 來控制滑動效果。

import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import PropTypes from 'prop-types';
import * as styles from './Swiper.module.scss';

const Swiper = ({
    items,
    loop = false,
    autoplay = false,
    delay = 3000,
    direction = 'horizontal',
    onSlideChange,
}) => {
    const [currentIndex, setCurrentIndex] = useState(0);
    const totalItems = items.length;

    useEffect(() => {
        let interval;
        if (autoplay) {
            interval = setInterval(() => {
                nextSlide();
            }, delay);
        }
        return () => clearInterval(interval);
    }, [currentIndex, autoplay, delay]);

    const nextSlide = () => {
        if (currentIndex < totalItems - 1) {
            setCurrentIndex(currentIndex + 1);
        } else if (loop) {
            setCurrentIndex(0);
        }
        if (onSlideChange) onSlideChange(currentIndex + 1);
    };

    const prevSlide = () => {
        if (currentIndex > 0) {
            setCurrentIndex(currentIndex - 1);
        } else if (loop) {
            setCurrentIndex(totalItems - 1);
        }
        if (onSlideChange) onSlideChange(currentIndex - 1);
    };

    return (
        <div className={`${styles.swiperContainer} ${direction === 'vertical' ? styles.vertical : ''}`}>
           <AnimatePresence>
                <motion.div
                    key={items[currentIndex].id}
                    className={styles.swiperSlide}
                    initial={{ opacity: 0, x: direction === 'horizontal' ? 100 : 0, y: direction === 'vertical' ? 100 : 0 }}
                    animate={{ opacity: 1, x: 0, y: 0 }}
                    exit={{ opacity: 0, x: direction === 'horizontal' ? -100 : 0, y: direction === 'vertical' ? -100 : 0 }}
                    transition={{ duration: 0.5 }}
                >
                    <img src={require(`@/assets/img/${items[currentIndex].image}`)} alt={items[currentIndex].title} className={styles.swiperImage} />
                    <div className={styles.swiperContent}>
                        <h2>{items[currentIndex].title}</h2>
                        <p>{items[currentIndex].description}</p>
                        <div className={styles.hashtags}>
                            {items[currentIndex].stack.map((tech, index) => (
                                <span key={index}>{tech}</span>
                            ))}
                        </div>
                        <div className={styles.pagination}>
                            <button onClick={prevSlide} disabled={!loop && currentIndex === 0}>← Prev</button>
                            <span>{currentIndex + 1} / {totalItems}</span>
                            <button onClick={nextSlide} disabled={!loop && currentIndex === totalItems - 1}>Next →</button>
                        </div>
                    </div>
                </motion.div>
            </AnimatePresence>
        </div>
    );
};

Swiper.propTypes = {
    items: PropTypes.arrayOf(
        PropTypes.shape({
            id: PropTypes.number.isRequired,
            title: PropTypes.string.isRequired,
            description: PropTypes.string.isRequired,
            image: PropTypes.string.isRequired,
            stack: PropTypes.arrayOf(PropTypes.string).isRequired,
        })
    ).isRequired,
    loop: PropTypes.bool,
    autoplay: PropTypes.bool,
    delay: PropTypes.number,
    direction: PropTypes.oneOf(['horizontal', 'vertical']),
    onSlideChange: PropTypes.func,
};

export default Swiper;

說明

  • items:滑動的內容,傳入每個專案的 idtitledescriptionimagestack
  • loop:是否啟用無限循環(預設為 false)。
  • autoplay:是否自動播放(預設為 false)。
  • delay:自動播放的延遲時間(預設為 3000 毫秒)。
  • direction:滑動方向,可以是 horizontalvertical
  • onSlideChange:滑動時觸發的回調函數,會傳回當前的 currentIndex

Step 2: 調整動畫與樣式

我們的 Swiper 元件需要根據 direction 的不同來實現不同方向的滑動效果。此外,若 autoplay 被啟用,我們需要確保滑動的過渡效果是流暢的。

@import '@/styles/variables';

.swiperContainer {
    position: relative;
    width: 100%;
    max-width: 800px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    overflow: hidden;
}

.swiperSlide {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 100%;
    transition: all 0.5s ease-in-out;
}

.swiperImage {
    width: 100%;
    max-width: 600px;
    height: auto;
    border-radius: 8px;
    object-fit: cover;
}

.swiperContent {
    margin-top: 20px;
}

.vertical .swiperSlide {
    flex-direction: column;
    height: 100%;
}

.pagination {
    display: flex;
    justify-content: space-between;
    width: 100%;
    margin-top: 20px;
}

說明

  • vertical 樣式:當滑動方向為 vertical 時,確保滑動內容以垂直方式顯示。
  • transition:滑動卡片的過渡效果設定為 0.5 秒,讓過渡更加流暢。

Step 3: 滑動手勢與鍵盤控制

為了提升用戶的操作體驗,我們可以添加滑動手勢支援和鍵盤導航功能,讓 Swiper 在行動裝置和桌機上都能輕鬆操作。

const [touchStart, setTouchStart] = useState(null);

const handleTouchStart = (e) => {
    const touchStartX = e.touches[0].clientX;
    setTouchStart(touchStartX);
};

const handleTouchEnd = (e) => {
    const touchEndX = e.changedTouches[0].clientX;
    if (touchStart - touchEndX > 50) {
        nextSlide();
    } else if (touchStart - touchEndX < -50) {
        prevSlide();
    }
};

useEffect(() => {
    const handleKeyDown = (e) => {
        if (e.key === 'ArrowRight') {
            nextSlide();
        } else if (e.key === 'ArrowLeft') {
            prevSlide();
        }
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
}, []);

說明:

  • 手勢控制:透過 touchStarttouchEnd 監控滑動手勢,當滑動距離超過 50px 時,觸發下一頁或上一頁。
  • 鍵盤控制:監聽左右箭頭按鍵,允許用戶通過鍵盤切換圖片。

結語

在這篇文章中,我們深入探討了如何從零開始建立一個滑動元件(Swiper),並展示如何通過自定義的 props 靈活控制其行為。這樣的元件讓我們能夠避免使用第三方庫,在不同的專案中保持靈活性,同時適應各種 UI 和功能需求。

在實務應用中,這種元件設計模式特別適合需要動態展示內容的頁面,讓用戶有更佳的瀏覽體驗。如果你對於更多的前端挑戰或元件設計有興趣,請繼續關注接下來的文章!


流光館Luma<∕> ✨ 期待與你繼續探索更多技術知識!



上一篇
Day 21 : 用 React 實現單頁應用的平滑滾動導航
下一篇
Day 23: 使用 API 管理 i18n,多語言支援的後端實作
系列文
從PM到前端開發:我的React作品集之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言